import { Path } from './Path';
import * as Util from '../../core/Util';
import * as LineUtil from '../../geometry/LineUtil';
import { LatLng, toLatLng } from '../../geo/LatLng';
import { LatLngBounds } from '../../geo/LatLngBounds';
import { Bounds } from '../../geometry/Bounds';
import { Point } from '../../geometry/Point';

/*
 * @class Polyline
 * @aka L.Polyline
 * @inherits Path
 *
 * A class for drawing polyline overlays on a map. Extends `Path`.
 *
 * @example
 *
 * ```js
 * // create a red polyline from an array of LatLng points
 * var latlngs = [
 * 	[45.51, -122.68],
 * 	[37.77, -122.43],
 * 	[34.04, -118.2]
 * ];
 *
 * var polyline = L.polyline(latlngs, {color: 'red'}).addTo(map);
 *
 * // zoom the map to the polyline
 * map.fitBounds(polyline.getBounds());
 * ```
 *
 * You can also pass a multi-dimensional array to represent a `MultiPolyline` shape:
 *
 * ```js
 * // create a red polyline from an array of arrays of LatLng points
 * var latlngs = [
 * 	[[45.51, -122.68],
 * 	 [37.77, -122.43],
 * 	 [34.04, -118.2]],
 * 	[[40.78, -73.91],
 * 	 [41.83, -87.62],
 * 	 [32.76, -96.72]]
 * ];
 * ```
 */

export var Polyline = Path.extend({
  // @section
  // @aka Polyline options
  options: {
    // @option smoothFactor: Number = 1.0
    // How much to simplify the polyline on each zoom level. More means
    // better performance and smoother look, and less means more accurate representation.
    smoothFactor: 1.0,

    // @option noClip: Boolean = false
    // Disable polyline clipping.
    noClip: false,
  },

  initialize: function (latlngs, options) {
    Util.setOptions(this, options);
    this._setLatLngs(latlngs);
  },

  // @method getLatLngs(): LatLng[]
  // Returns an array of the points in the path, or nested arrays of points in case of multi-polyline.
  getLatLngs: function () {
    return this._latlngs;
  },

  // @method setLatLngs(latlngs: LatLng[]): this
  // Replaces all the points in the polyline with the given array of geographical points.
  setLatLngs: function (latlngs) {
    this._setLatLngs(latlngs);
    return this.redraw();
  },

  // @method isEmpty(): Boolean
  // Returns `true` if the Polyline has no LatLngs.
  isEmpty: function () {
    return !this._latlngs.length;
  },

  // @method closestLayerPoint(p: Point): Point
  // Returns the point closest to `p` on the Polyline.
  closestLayerPoint: function (p) {
    var minDistance = Infinity,
      minPoint = null,
      closest = LineUtil._sqClosestPointOnSegment,
      p1,
      p2;

    for (var j = 0, jLen = this._parts.length; j < jLen; j++) {
      var points = this._parts[j];

      for (var i = 1, len = points.length; i < len; i++) {
        p1 = points[i - 1];
        p2 = points[i];

        var sqDist = closest(p, p1, p2, true);

        if (sqDist < minDistance) {
          minDistance = sqDist;
          minPoint = closest(p, p1, p2);
        }
      }
    }
    if (minPoint) {
      minPoint.distance = Math.sqrt(minDistance);
    }
    return minPoint;
  },

  // @method getCenter(): LatLng
  // Returns the center ([centroid](https://en.wikipedia.org/wiki/Centroid)) of the polyline.
  getCenter: function () {
    // throws error when not yet added to map as this center calculation requires projected coordinates
    if (!this._map) {
      throw new Error('Must add layer to map before using getCenter()');
    }
    return LineUtil.polylineCenter(this._defaultShape(), this._map.options.crs);
  },

  // @method getBounds(): LatLngBounds
  // Returns the `LatLngBounds` of the path.
  getBounds: function () {
    return this._bounds;
  },

  // @method addLatLng(latlng: LatLng, latlngs?: LatLng[]): this
  // Adds a given point to the polyline. By default, adds to the first ring of
  // the polyline in case of a multi-polyline, but can be overridden by passing
  // a specific ring as a LatLng array (that you can earlier access with [`getLatLngs`](#polyline-getlatlngs)).
  addLatLng: function (latlng, latlngs) {
    latlngs = latlngs || this._defaultShape();
    latlng = toLatLng(latlng);
    latlngs.push(latlng);
    this._bounds.extend(latlng);
    return this.redraw();
  },

  _setLatLngs: function (latlngs) {
    this._bounds = new LatLngBounds();
    this._latlngs = this._convertLatLngs(latlngs);
  },

  _defaultShape: function () {
    return LineUtil.isFlat(this._latlngs) ? this._latlngs : this._latlngs[0];
  },

  // recursively convert latlngs input into actual LatLng instances; calculate bounds along the way
  _convertLatLngs: function (latlngs) {
    var result = [],
      flat = LineUtil.isFlat(latlngs);

    for (var i = 0, len = latlngs.length; i < len; i++) {
      if (flat) {
        result[i] = toLatLng(latlngs[i]);
        this._bounds.extend(result[i]);
      } else {
        result[i] = this._convertLatLngs(latlngs[i]);
      }
    }

    return result;
  },

  _project: function () {
    var pxBounds = new Bounds();
    this._rings = [];
    this._projectLatlngs(this._latlngs, this._rings, pxBounds);

    if (this._bounds.isValid() && pxBounds.isValid()) {
      this._rawPxBounds = pxBounds;
      this._updateBounds();
    }
  },

  _updateBounds: function () {
    var w = this._clickTolerance(),
      p = new Point(w, w);

    if (!this._rawPxBounds) {
      return;
    }

    this._pxBounds = new Bounds([
      this._rawPxBounds.min.subtract(p),
      this._rawPxBounds.max.add(p),
    ]);
  },

  // recursively turns latlngs into a set of rings with projected coordinates
  _projectLatlngs: function (latlngs, result, projectedBounds) {
    var flat = latlngs[0] instanceof LatLng,
      len = latlngs.length,
      i,
      ring;

    if (flat) {
      ring = [];
      for (i = 0; i < len; i++) {
        ring[i] = this._map.latLngToLayerPoint(latlngs[i]);
        projectedBounds.extend(ring[i]);
      }
      result.push(ring);
    } else {
      for (i = 0; i < len; i++) {
        this._projectLatlngs(latlngs[i], result, projectedBounds);
      }
    }
  },

  // clip polyline by renderer bounds so that we have less to render for performance
  _clipPoints: function () {
    var bounds = this._renderer._bounds;

    this._parts = [];
    if (!this._pxBounds || !this._pxBounds.intersects(bounds)) {
      return;
    }

    if (this.options.noClip) {
      this._parts = this._rings;
      return;
    }

    var parts = this._parts,
      i,
      j,
      k,
      len,
      len2,
      segment,
      points;

    for (i = 0, k = 0, len = this._rings.length; i < len; i++) {
      points = this._rings[i];

      for (j = 0, len2 = points.length; j < len2 - 1; j++) {
        segment = LineUtil.clipSegment(
          points[j],
          points[j + 1],
          bounds,
          j,
          true,
        );

        if (!segment) {
          continue;
        }

        parts[k] = parts[k] || [];
        parts[k].push(segment[0]);

        // if segment goes out of screen, or it's the last one, it's the end of the line part
        if (segment[1] !== points[j + 1] || j === len2 - 2) {
          parts[k].push(segment[1]);
          k++;
        }
      }
    }
  },

  // simplify each clipped part of the polyline for performance
  _simplifyPoints: function () {
    var parts = this._parts,
      tolerance = this.options.smoothFactor;

    for (var i = 0, len = parts.length; i < len; i++) {
      parts[i] = LineUtil.simplify(parts[i], tolerance);
    }
  },

  _update: function () {
    if (!this._map) {
      return;
    }

    this._clipPoints();
    this._simplifyPoints();
    this._updatePath();
  },

  _updatePath: function () {
    this._renderer._updatePoly(this);
  },

  // Needed by the `Canvas` renderer for interactivity
  _containsPoint: function (p, closed) {
    var i,
      j,
      k,
      len,
      len2,
      part,
      w = this._clickTolerance();

    if (!this._pxBounds || !this._pxBounds.contains(p)) {
      return false;
    }

    // hit detection for polylines
    for (i = 0, len = this._parts.length; i < len; i++) {
      part = this._parts[i];

      for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) {
        if (!closed && j === 0) {
          continue;
        }

        if (LineUtil.pointToSegmentDistance(p, part[k], part[j]) <= w) {
          return true;
        }
      }
    }
    return false;
  },
});

// @factory L.polyline(latlngs: LatLng[], options?: Polyline options)
// Instantiates a polyline object given an array of geographical points and
// optionally an options object. You can create a `Polyline` object with
// multiple separate lines (`MultiPolyline`) by passing an array of arrays
// of geographic points.
export function polyline(latlngs, options) {
  return new Polyline(latlngs, options);
}

// Retrocompat. Allow plugins to support Leaflet versions before and after 1.1.
Polyline._flat = LineUtil._flat;
